/**
 * PANDA 3D SOFTWARE
 * Copyright (c) Carnegie Mellon University.  All rights reserved.
 *
 * All use of this software is subject to the terms of the revised BSD
 * license.  You should have received a copy of this license along
 * with this source code in a file named "LICENSE."
 *
 * @file geoMipTerrain.I
 * @author rdb
 * @date 2007-06-29
 */

#include "config_grutil.h"

/**
 *
 */
INLINE GeoMipTerrain::
GeoMipTerrain(const std::string &name) {
  _root = NodePath(name);
  _root_flattened = false;
  _xsize = 0;
  _ysize = 0;
  _block_size = 16;
  _max_level = 4; // Always log(_block_size) / log(2.0)
  _min_level = 0;
  _factor = 100.0;
  _near = 16.0;
  _far = 128.0;
  _use_near_far = false;
  _has_color_map = false;
  PT(PandaNode) tmpnode = new PandaNode("tmp_focal");
  _auto_flatten = AFM_off;
  _focal_point = NodePath(tmpnode);
  _focal_is_temporary = true;
  _is_dirty = true;
  _bruteforce = false;
  _stitching = false;
}

/**
 * This will not remove the terrain node itself.  To have the terrain itself
 * also deleted, please call remove_node() prior to destruction.
 */
INLINE GeoMipTerrain::
~GeoMipTerrain() {
}

/**
 * Returns a reference to the heightfield (a PNMImage) contained inside
 * GeoMipTerrain.  You can use the reference to alter the heightfield.
 */
INLINE PNMImage &GeoMipTerrain::
heightfield() {
  return _heightfield;
}

/**
 * Returns a reference to the color map (a PNMImage) contained inside
 * GeoMipTerrain.  You can use the reference to alter the color map.
 */
INLINE PNMImage &GeoMipTerrain::
color_map() {
  return _color_map;
}

/**
 * Sets a boolean specifying whether the terrain will be rendered bruteforce.
 * If the terrain is rendered bruteforce, there will be no Level of Detail,
 * and the update() call will only update the terrain if it is marked dirty.
 */
INLINE void GeoMipTerrain::
set_bruteforce(bool bf) {
  if (bf == true && _bruteforce == false) {
    _is_dirty = true;
  }
  _bruteforce = bf;
}

/**
 * Returns a boolean whether the terrain is rendered bruteforce or not.  See
 * set_bruteforce for more information.
 */
INLINE bool GeoMipTerrain::
get_bruteforce() {
  return _bruteforce;
}

/**
 * The terrain can be automatically flattened (using flatten_light,
 * flatten_medium, or flatten_strong) after each update.  This only affects
 * future updates, it doesn't flatten the current terrain.
 */
INLINE void GeoMipTerrain::
set_auto_flatten(int mode) {
  _auto_flatten = mode;
}

/**
 * Sets the focal point.  GeoMipTerrain generates high-resolution terrain
 * around the focal point, and progressively lower and lower resolution
 * terrain as you get farther away.  If a point is supplied and not a
 * NodePath, make sure it's relative to the terrain.  Only the x and y
 * coordinates of the focal point are taken in respect.
 */
INLINE void GeoMipTerrain::
set_focal_point(double x, double y) {
  if (!_focal_is_temporary) {
    PT(PandaNode) tmpnode = new PandaNode("tmp_focal");
    _focal_point = NodePath(tmpnode);
  }
  _focal_point.set_pos(_root, x, y, 0);
  _focal_is_temporary = true;
}
INLINE void GeoMipTerrain::
set_focal_point(const LPoint2d &fp) {
  set_focal_point(fp.get_x(), fp.get_y());
}
INLINE void GeoMipTerrain::
set_focal_point(const LPoint2f &fp) {
  set_focal_point(double(fp.get_x()), double(fp.get_y()));
}
INLINE void GeoMipTerrain::
set_focal_point(const LPoint3d &fp) {
  set_focal_point(fp.get_x(), fp.get_y());
}
INLINE void GeoMipTerrain::
set_focal_point(const LPoint3f &fp) {
  set_focal_point(double(fp.get_x()), double(fp.get_y()));
}
INLINE void GeoMipTerrain::
set_focal_point(NodePath fp) {
  if (_focal_is_temporary) {
    _focal_point.remove_node();
  }
  _focal_point = fp;
  _focal_is_temporary = false;
}

/**
 * Returns the focal point, as a NodePath.  If you have set it to be just a
 * point, it will return an empty node at the focal position.
 */
INLINE NodePath GeoMipTerrain::
get_focal_point() const {
  return _focal_point;
}

/**
 * Returns the root of the terrain.  This is a single PandaNode to which all
 * the rest of the terrain is parented.  The generate and update operations
 * replace the nodes which are parented to this root, but they don't replace
 * this root itself.
 */
INLINE NodePath GeoMipTerrain::
get_root() const {
  return _root;
}

/**
 * Sets the minimum level of detail at which blocks may be generated by
 * generate() or update(). The default value is 0, which is the highest
 * quality.  This value is also taken in respect when generating the terrain
 * bruteforce.
 */
INLINE void GeoMipTerrain::
set_min_level(unsigned short minlevel) {
  _min_level = minlevel;
}

/**
 * Gets the minimum level of detail at which blocks may be generated by
 * generate() or update(). The default value is 0, which is the highest
 * quality.
 */
INLINE unsigned short GeoMipTerrain::
get_min_level() {
  return _min_level;
}

/**
 * Returns the highest level possible for this block size.  When a block is at
 * this level, it will be the worst quality possible.
 */
INLINE unsigned short GeoMipTerrain::
get_max_level() {
  return _max_level;
}

/**
 * Gets the block size.
 */
INLINE unsigned short GeoMipTerrain::
get_block_size() {
  return _block_size;
}

/**
 * Sets the block size.  If it is not a power of two, the closest power of two
 * is used.
 */
INLINE void GeoMipTerrain::
set_block_size(unsigned short newbs) {
  if (is_power_of_two(newbs)) {
    _block_size = newbs;
  } else {
    if (is_power_of_two(newbs - 1)) {
      _block_size = newbs - 1;
    } else {
      if (is_power_of_two(newbs + 1)) {
        _block_size = newbs + 1;
      } else {
        _block_size = (unsigned short) pow(2.0,
                            floor(log((double) newbs) / log(2.0) + 0.5));
      }
    }
  }
  _max_level = (unsigned short) (log((double) _block_size) / log(2.0));
  _is_dirty = true;
}

/**
 * Returns a bool indicating whether the terrain is marked 'dirty', that means
 * the terrain has to be regenerated on the next update() call, because for
 * instance the heightfield has changed.  Once the terrain has been
 * regenerated, the dirty flag automatically gets reset internally.
 */
INLINE bool GeoMipTerrain::
is_dirty() {
  return _is_dirty;
}

/**
 * DEPRECATED method.  Use set_near/far instead.  Sets the quality factor at
 * which blocks must be generated.  The higher this level, the better quality
 * the terrain will be, but more expensive to render.  A value of 0 makes the
 * terrain the lowest quality possible, depending on blocksize.  The default
 * value is 100.
 *
 * @deprecated use set_near/set_far instead
 */
INLINE void GeoMipTerrain::
set_factor(PN_stdfloat factor) {
  if (grutil_cat.is_debug()) {
    grutil_cat.debug() << "Using deprecated method set_factor, use set_near and set_far instead!\n";
  }
  _use_near_far = false;
  _factor = factor;
}

/**
 * Sets the near and far LOD distances in one call.
 */
INLINE void GeoMipTerrain::
set_near_far(double input_near, double input_far) {
  _use_near_far = true;
  _near = input_near;
  _far = input_far;
}

/**
 * Sets the near LOD distance, at which the terrain will be rendered at
 * highest quality.  This distance is in the terrain's coordinate space!
 */
INLINE void GeoMipTerrain::
set_near(double input_near) {
  _use_near_far = true;
  _near = input_near;
}

/**
 * Sets the far LOD distance, at which the terrain will be rendered at lowest
 * quality.  This distance is in the terrain's coordinate space!
 */
INLINE void GeoMipTerrain::
set_far(double input_far) {
  _use_near_far = true;
  _far = input_far;
}

/**
 * Returns the far LOD distance in the terrain coordinate space
 */
INLINE double GeoMipTerrain::
get_far() {
  return _far;
}

/**
 * Returns the near LOD distance in the terrain coordinate space
 */
INLINE double GeoMipTerrain::
get_near() {
  return _near;
}

/**
 * Returns the automatic-flatten mode (e.g., off, flatten_light,
 * flatten_medium, or flatten_strong)
 */
INLINE int GeoMipTerrain::
get_flatten_mode() {
  return _auto_flatten;
}

/**
 * Returns the NodePath of the specified block.  If auto-flatten is enabled
 * and the node is getting removed during the flattening process, it will
 * still return a NodePath with the appropriate terrain chunk, but it will be
 * in a temporary scenegraph.  Please note that this returns a const object
 * and you can not modify the node.  Modify the heightfield instead.
 */
INLINE const NodePath GeoMipTerrain::
get_block_node_path(unsigned short mx, unsigned short my) {
  nassertr(mx < _blocks.size(), NodePath::fail());
  nassertr(my < _blocks[mx].size(), NodePath::fail());
  return _blocks[mx][my];
}

/**
 * Gets the coordinates of the block at the specified position.  This position
 * must be relative to the terrain, not to render.  Returns an array
 * containing two values: the block x and the block y coords.  If the
 * positions are out of range, the closest block is taken.  Note that the
 * VecBase returned does not represent a vector, position, or rotation, but it
 * contains the block index of the block which you can use in
 * GeoMipTerrain::get_block_node_path.
 */
INLINE LVecBase2 GeoMipTerrain::
get_block_from_pos(double x, double y) {
  if (x < 0) x = 0;
  if (y < 0) y = 0;
  if (x > _xsize - 1) x = _xsize - 1;
  if (y > _ysize - 1) y = _ysize - 1;
  x = floor(x / _block_size);
  y = floor(y / _block_size);
  return LVecBase2(x, y);
}
/**
 * Calculates the level for the given mipmap.
 */
INLINE unsigned short GeoMipTerrain::
lod_decide(unsigned short mx, unsigned short my) {
  PN_stdfloat cx = mx;
  PN_stdfloat cy = my;
  cx = (cx * _block_size + _block_size / 2) * _root.get_sx();
  cy = (cy * _block_size + _block_size / 2) * _root.get_sy();
  PN_stdfloat d;
  if (_use_near_far) {
    d = sqrt(pow(_focal_point.get_x(_root) - cx, 2) +
             pow(_focal_point.get_y(_root) - cy, 2));
    if (d < _near) {
      return 0;
    } else if (d > _far) {
      return _max_level;
    } else {
      return (unsigned short)((d - _near) / (_far - _near) * _max_level * (1.0 - (_min_level / _max_level)) + _min_level);
    }
  } else {
    if (_factor > 0.0) {
      d = sqrt(pow(_focal_point.get_x(_root) - cx, 2) +
               pow(_focal_point.get_y(_root) - cy, 2)) / _factor;
    } else {
      d = _max_level;
    }
    return short(floor(d));
  }
}

/**
 * Loads the specified heightmap image file into the heightfield.  Returns
 * true if succeeded, or false if an error has occured.  If the heightmap is
 * not a power of two plus one, it is scaled up using a gaussian filter.
 */
INLINE bool GeoMipTerrain::
set_heightfield(const PNMImage &image) {
  if (image.get_color_space() == CS_sRGB) {
    // Probably a mistaken metadata setting on the file.
    grutil_cat.warning()
      << "Heightfield image is specified to have sRGB color space!\n"
         "Panda applies gamma correction, which will probably cause "
         "it to produce incorrect results.\n";
  }

  // Before we apply anything, validate the size.
  if (is_power_of_two(image.get_x_size() - 1) &&
      is_power_of_two(image.get_y_size() - 1)) {
    _heightfield = image;
    _is_dirty = true;
    _xsize = _heightfield.get_x_size();
    _ysize = _heightfield.get_y_size();
    return true;
  } else {
    grutil_cat.error()
      << "Specified image does not have a power-of-two-plus-one size!\n";
  }
  return false;
}

/**
 * Loads the specified image as color map.  The next time generate() is
 * called, the terrain is painted with this color map using the vertex color
 * column.  Returns a boolean indicating whether the operation has succeeded.
 */
INLINE bool GeoMipTerrain::
set_color_map(const Filename &filename, PNMFileType *ftype) {
  if (_color_map.read(filename, ftype)) {
    _is_dirty = true;
    _has_color_map = true;
    return true;
  }
  return false;
}

INLINE bool GeoMipTerrain::
set_color_map(const PNMImage &image) {
  _color_map.copy_from(image);
  _is_dirty = true;
  _has_color_map = true;
  return true;
}

INLINE bool GeoMipTerrain::
set_color_map(const Texture *tex) {
  tex->store(_color_map);
  _is_dirty = true;
  return true;
}

INLINE bool GeoMipTerrain::
set_color_map(const std::string &path) {
  return set_color_map(Filename(path));
}

/**
 * Returns whether a color map has been set.
 */
INLINE bool GeoMipTerrain::
has_color_map() const {
  return _has_color_map;
}

/**
 * Clears the color map.
 */
INLINE void GeoMipTerrain::
clear_color_map() {
  if (_has_color_map) {
    _color_map.clear();
    _has_color_map = false;
  }
}

/**
 * If this value is true, the LOD level at the borders of the terrain will be
 * 0. This is useful if you have multiple terrains attached and you want to
 * stitch them together, to fix seams.  This setting also has effect when
 * bruteforce is enabled, although in that case you are probably better off
 * with setting the minlevels to the same value.
 */
INLINE void GeoMipTerrain::
set_border_stitching(bool stitching) {
  if (stitching && !_stitching) {
    _is_dirty = true;
  }
  _stitching = stitching;
}

/**
 * Returns the current stitching setting.  False by default, unless
 * set_stitching has been set.
 */
INLINE bool GeoMipTerrain::
get_border_stitching() {
  return _stitching;
}

/**
 * Get the elevation at a certain pixel of the image.  This function does NOT
 * linearly interpolate.  For that, use GeoMipTerrain::get_elevation()
 * instead.
 */
INLINE double GeoMipTerrain::
get_pixel_value(int x, int y) {
  x = std::max(std::min(x,int(_xsize-1)),0);
  y = std::max(std::min(y,int(_ysize-1)),0);
  if (_heightfield.is_grayscale()) {
    return double(_heightfield.get_bright(x, y));
  } else {
    return double(_heightfield.get_red(x, y))
         + double(_heightfield.get_green(x, y)) / 256.0
         + double(_heightfield.get_blue(x, y)) / 65536.0;
  }
}
INLINE double GeoMipTerrain::
get_pixel_value(unsigned short mx, unsigned short my, int x, int y) {
  nassertr_always(mx < (_xsize - 1) / _block_size, false);
  nassertr_always(my < (_ysize - 1) / _block_size, false);
  return get_pixel_value(mx * _block_size + x, (_ysize - 1) -
                         (my * _block_size + y));
}

/**
 * Fetches the terrain normal at (x,y), where the input coordinate is
 * specified in pixels.  This ignores the current LOD level and instead
 * provides an accurate number.  Terrain scale is NOT taken into account!  To
 * get accurate normals, please divide it by the terrain scale and normalize
 * it again!
 */
INLINE LVector3 GeoMipTerrain::
get_normal(unsigned short mx, unsigned short my, int x, int y) {
  nassertr_always(mx < (_xsize - 1) / _block_size, false);
  nassertr_always(my < (_ysize - 1) / _block_size, false);
  return get_normal(mx * _block_size + x, (_ysize - 1) -
                    (my * _block_size + y));
}

/**
 * Returns a bool whether the given int i is a power of two or not.
 */
INLINE bool GeoMipTerrain::
is_power_of_two(unsigned int i) {
  return !((i - 1) & i);
}

/**
 * Returns the part of the number right of the floating-point.
 */
INLINE float GeoMipTerrain::
f_part(float i) {
  return i - floor(i);
}
INLINE double GeoMipTerrain::
f_part(double i) {
  return i - floor(i);
}

/**
 * Used to calculate vertex numbers.  Only to be used internally.
 */
INLINE int GeoMipTerrain::
sfav(int n, int powlevel, int mypowlevel) {
  double t = n - 1;
  t /= pow(2.0, powlevel - mypowlevel);
  t = double(int(t > 0.0 ? t + 0.5 : t - 0.5));
  t *= pow(2.0, powlevel - mypowlevel);
  return int(t);
}
